Skip to content

Batch Approval for AI Tool Calls, fix "AI is thinking" message, chunk JS#2430

Merged
sawka merged 6 commits intomainfrom
sawka/batch-approval
Oct 14, 2025
Merged

Batch Approval for AI Tool Calls, fix "AI is thinking" message, chunk JS#2430
sawka merged 6 commits intomainfrom
sawka/batch-approval

Conversation

@sawka
Copy link
Member

@sawka sawka commented Oct 14, 2025

We now show all Read File/Dir calls together and batch approve them (backend change to emit them all at once, and FE change to display them as a batch)

JS chunking for monaco, mermaid, and shiki, etc. shiki is huge, almost 10M but can't be easily split out of Streamdown. Tried making it load async, but w/ Streamdown we can't do that easily. Trying to split the JS up because of a build error we were running into in build-helper.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

Walkthrough

  • .gitignore: adds an additional ignore entry for "out".
  • electron.vite.config.ts: adds a renderer.build.rollupOptions.output.manualChunks function to group node_modules into named chunks for monaco, mermaid, katex, shiki, and cytoscape.
  • frontend/app/aipanel/aimessage.tsx: substantial refactor introducing grouping of consecutive data-tooluse parts, new components (AIToolUseBatch, AIToolUseGroup, AIToolUse, AIToolApprovalButtons, AIToolUseBatchItem, AIThinking message.prop support), updated thinking-state derivation, and revised rendering to use grouped logic and conditional thinking prompts.
  • frontend/app/aipanel/aipanel.tsx: formatting-only change to AIPanelMessages usage.
  • pkg/aiusechat/openai/openai-backend.go: removes SSE emission and a log line for the "response.function_call_arguments.done" event while still constructing and storing ToolUseData.
  • pkg/aiusechat/usechat.go: adds a preprocessing step that broadcasts existing ToolUseData for all tool calls (sending "data-tooluse" SSE events) before per-tool execution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarizes the three main modifications introduced by this changeset: batch approval for AI tool calls, a fix to the “AI is thinking” message, and JavaScript chunk splitting. Each element corresponds directly to substantial updates in the backend, UI components, and Vite configuration. This concise phrasing enables teammates to quickly understand the primary focus of the pull request.
Description Check ✅ Passed The description directly outlines the batching of Read File/Dir calls in both backend and frontend and explains the rationale for chunking large JS modules like Shiki to resolve build errors. It matches the actual code changes across configuration and component files and provides context for why these adjustments were made. This confirms that the description is relevant and appropriately reflects the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/batch-approval

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
pkg/aiusechat/usechat.go (1)

313-319: Persist initial ToolUseData to chat store as well

Pre-emitting SSE is great. Consider also persisting the initial ToolUseData so a reconnect/history load reflects it without relying on prior SSE.

Apply this diff to also update the chat:

 for _, toolCall := range stopReason.ToolCalls {
     if toolCall.ToolUseData != nil {
         log.Printf("AI data-tooluse %s\n", toolCall.ID)
         _ = sseHandler.AiMsgData("data-tooluse", toolCall.ID, *toolCall.ToolUseData)
+        updateToolUseDataInChat(chatOpts, toolCall.ID, toolCall.ToolUseData)
     }
 }
frontend/app/aipanel/aimessage.tsx (2)

121-127: Group title can be wrong when mixing file reads and directory listings

A single batch may contain both read_text_file and read_dir; using the first item to pick title can mislabel the group.

Split batches by toolname (reads vs listings) or compute a combined label, e.g., “Reading files and listing directories”.


128-149: Prefer stable keys (toolcallid) over array indices

Index keys cause unnecessary re-renders and can break UI state when arrays change.

Apply this diff:

-                    {parts.map((part, idx) => {
+                    {parts.map((part) => {
@@
-                            <div key={idx} className="text-sm pl-2">
+                            <div key={part.data.toolcallid} className="text-sm pl-2">
-            {otherTools.map((tool, idx) => (
-                <div key={idx} className="mt-2">
+            {otherTools.map((tool) => (
+                <div key={tool.data.toolcallid} className="mt-2">

Also applies to: 290-293

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df40046 and 46ee88b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • electron.vite.config.ts (1 hunks)
  • frontend/app/aipanel/aimessage.tsx (6 hunks)
  • frontend/app/aipanel/aipanel.tsx (1 hunks)
  • pkg/aiusechat/openai/openai-backend.go (0 hunks)
  • pkg/aiusechat/usechat.go (1 hunks)
💤 Files with no reviewable changes (1)
  • pkg/aiusechat/openai/openai-backend.go
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aipanel.tsx (1)
frontend/app/aipanel/aipanelmessages.tsx (1)
  • AIPanelMessages (16-64)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx

[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
.gitignore (1)

20-20: Adding plain out ignore looks fine

Covers both out/ dir and an out file. Just confirm no legitimate top-level file named out needs to be tracked.

frontend/app/aipanel/aipanel.tsx (1)

532-536: Prop reflow only — no functional change

LGTM. Keeps props explicit and readable; no behavior impact.

frontend/app/aipanel/aimessage.tsx (1)

13-21: AIThinking prop is a nice improvement

Parametric message with a safe guard is clean. LGTM.

Comment on lines +133 to +146
output: {
manualChunks(id) {
if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
return "mermaid";
if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
return "shiki";
}
if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
return "cytoscape";
return undefined;
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

manualChunks path checks may miss packages and Windows paths

  • Windows backslashes can break id.includes("node_modules/...").
  • Likely scopes are different: @shikijs (not @shiki), @monaco-editor (not @monaco), @mermaid-js (not @mermaid).

Use normalized paths and correct scopes:

-                output: {
-                    manualChunks(id) {
-                        if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
-                        if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
-                            return "mermaid";
-                        if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
-                        if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
-                            return "shiki";
-                        }
-                        if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
-                            return "cytoscape";
-                        return undefined;
-                    },
-                },
+                output: {
+                    manualChunks(id) {
+                        const p = id.replace(/\\/g, "/");
+                        if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor"))
+                            return "monaco";
+                        if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js"))
+                            return "mermaid";
+                        if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex")) return "katex";
+                        if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs")) return "shiki";
+                        if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape"))
+                            return "cytoscape";
+                        return undefined;
+                    },
+                },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
output: {
manualChunks(id) {
if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
return "mermaid";
if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
return "shiki";
}
if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
return "cytoscape";
return undefined;
},
},
output: {
manualChunks(id) {
// normalize Windows backslashes to forward slashes
const p = id.replace(/\\/g, "/");
if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor"))
return "monaco";
if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js"))
return "mermaid";
if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex"))
return "katex";
if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs"))
return "shiki";
if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape"))
return "cytoscape";
return undefined;
},
},

Comment on lines +79 to +100
if (parts.length === 0) return null;

const firstTool = parts[0].data;
const baseApproval = userApprovalOverride || firstTool.approval;
const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;
const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval");

useEffect(() => {
if (!isStreaming || effectiveApproval !== "needs-approval") return;

const interval = setInterval(() => {
parts.forEach((part) => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: part.data.toolcallid,
keepalive: true,
});
});
}, 4000);

return () => clearInterval(interval);
}, [isStreaming, effectiveApproval, parts]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix hook-order violation in AIToolUseBatch (early return before useEffect)

The early return precedes useEffect, violating hooks rule (matches static analysis hint). Always call hooks in the same order.

Apply this diff:

-    if (parts.length === 0) return null;
+    const hasParts = parts.length > 0;
@@
-    useEffect(() => {
+    useEffect(() => {
         if (!isStreaming || effectiveApproval !== "needs-approval") return;
@@
-    }, [isStreaming, effectiveApproval, parts]);
+    }, [isStreaming, effectiveApproval, parts]);
+
+    if (!hasParts) return null;

Based on static analysis hints

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
frontend/app/aipanel/aimessage.tsx around lines 79 to 100: the component
currently returns early when parts.length === 0 which prevents hooks from being
called in a consistent order; remove the early return and instead compute a
boolean like isEmpty = parts.length === 0 so that useEffect and other hooks are
always invoked unconditionally, keep the same useEffect logic and dependencies,
and finally render null (return null) at the JSX/return point when isEmpty is
true.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/app/aipanel/aimessage.tsx (1)

121-123: Batch title can be inaccurate for mixed file ops

If the batch contains both read_text_file and read_dir, the title uses only the first item.

Replace with:

-    const groupTitle = firstTool.toolname === "read_text_file" ? "Reading Files" : "Listing Directories";
+    const hasReadFile = parts.some((p) => p.data.toolname === "read_text_file");
+    const hasReadDir = parts.some((p) => p.data.toolname === "read_dir");
+    const groupTitle = hasReadFile && hasReadDir ? "File Access" : hasReadFile ? "Reading Files" : "Listing Directories";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df40046 and 46ee88b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • electron.vite.config.ts (1 hunks)
  • frontend/app/aipanel/aimessage.tsx (6 hunks)
  • frontend/app/aipanel/aipanel.tsx (1 hunks)
  • pkg/aiusechat/openai/openai-backend.go (0 hunks)
  • pkg/aiusechat/usechat.go (1 hunks)
💤 Files with no reviewable changes (1)
  • pkg/aiusechat/openai/openai-backend.go
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
frontend/app/aipanel/aipanel.tsx (1)
frontend/app/aipanel/aipanelmessages.tsx (1)
  • AIPanelMessages (16-64)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx

[error] 86-86: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (4)
.gitignore (1)

20-20: No issues found—good safeguard.

Adding the bare out entry neatly covers files alongside the existing out/ directory rule.

frontend/app/aipanel/aipanel.tsx (1)

532-536: Formatting-only change looks good

Props preserved; no behavior change.

pkg/aiusechat/usechat.go (1)

313-319: Pre-broadcast of tooluse events — LGTM

Early emission enables batching; guarded against nil. Ensure upstream provider handlers no longer emit initial data-tooluse to avoid duplicates.

Can you confirm that openai and anthropic paths no longer emit initial data-tooluse via SSE (only updates), so the UI won’t double-render initial entries?

frontend/app/aipanel/aimessage.tsx (1)

371-413: Thinking-state derivation looks solid

Nice prioritization: approvals > post-step content > reasoning. Clean integration with AIThinking.

Comment on lines +133 to +146
output: {
manualChunks(id) {
if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
return "mermaid";
if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
return "shiki";
}
if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
return "cytoscape";
return undefined;
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Chunk matchers miss Monaco and scoped packages; normalize paths

  • Monaco is published as monaco-editor, not monaco; current checks won’t match.
  • On Windows, ids may contain backslashes.
  • Mermaid is commonly @mermaid-js/; Shiki often under @shikijs/.

Refine matching and normalize separators.

Apply this diff to harden matching:

-                output: {
-                    manualChunks(id) {
-                        if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
-                        if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
-                            return "mermaid";
-                        if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
-                        if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
-                            return "shiki";
-                        }
-                        if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
-                            return "cytoscape";
-                        return undefined;
-                    },
-                },
+                output: {
+                    manualChunks(id) {
+                        const p = id.replace(/\\/g, "/");
+                        if (p.includes("/node_modules/monaco-editor")) return "monaco";
+                        if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/"))
+                            return "mermaid";
+                        if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex";
+                        if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) return "shiki";
+                        if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/"))
+                            return "cytoscape";
+                        return undefined;
+                    },
+                },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
output: {
manualChunks(id) {
if (id.includes("node_modules/monaco") || id.includes("node_modules/@monaco")) return "monaco";
if (id.includes("node_modules/mermaid") || id.includes("node_modules/@mermaid"))
return "mermaid";
if (id.includes("node_modules/katex") || id.includes("node_modules/@katex")) return "katex";
if (id.includes("node_modules/shiki") || id.includes("node_modules/@shiki")) {
return "shiki";
}
if (id.includes("node_modules/cytoscape") || id.includes("node_modules/@cytoscape"))
return "cytoscape";
return undefined;
},
},
output: {
manualChunks(id) {
const p = id.replace(/\\/g, "/");
if (p.includes("/node_modules/monaco-editor")) return "monaco";
if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/"))
return "mermaid";
if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex";
if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) return "shiki";
if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/"))
return "cytoscape";
return undefined;
},
},
🤖 Prompt for AI Agents
In electron.vite.config.ts around lines 133 to 146, the manualChunks path checks
miss actual package names and Windows backslashes; normalize id by replacing
backslashes with forward slashes, and match real package names and common scoped
names: check for "monaco-editor" and both "@monaco" and "@monaco-editor" scopes,
"@mermaid-js" (and "mermaid"), "@katex" or "katex", "@shikijs" (and "shiki"),
and "@cytoscape" (and "cytoscape"); return the appropriate chunk names when
those normalized paths include the target strings. Ensure you normalize id once
at the top of manualChunks and use strict includes against the normalized path.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
frontend/app/aipanel/aimessage.tsx (2)

135-158: Fix Rule of Hooks violation and use “any needs approval” for keepalive

  • useEffect is declared after an early return; violates hook order.
  • Keepalive is gated by group-level approval based on the first tool; others may starve. Use anyNeedApproval.

Apply:

 const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => {
-    const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
-
-    if (parts.length === 0) return null;
-
-    const firstTool = parts[0].data;
-    const baseApproval = userApprovalOverride || firstTool.approval;
-    const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;
-    const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval");
-
-    useEffect(() => {
-        if (!isStreaming || effectiveApproval !== "needs-approval") return;
+    const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
+    const hasParts = parts.length > 0;
+    const anyNeedApproval = parts.some((p) => (userApprovalOverride || p.data.approval) === "needs-approval");
+    const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval");
+
+    useEffect(() => {
+        if (!isStreaming || !anyNeedApproval) return;
 
         const interval = setInterval(() => {
             parts.forEach((part) => {
                 RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
                     toolcallid: part.data.toolcallid,
                     keepalive: true,
                 });
             });
         }, 4000);
 
         return () => clearInterval(interval);
-    }, [isStreaming, effectiveApproval, parts]);
+    }, [isStreaming, anyNeedApproval, parts]);
+
+    if (!hasParts) return null;

Based on static analysis hints


184-191: Per-part approval for status/error and approval UI gating

  • Items currently receive a group-level effectiveApproval; errors/timeouts can be misreported.
  • Show Approve All only when all parts still need approval.

Apply:

-                <div className="mt-1 space-y-0.5">
-                    {parts.map((part, idx) => (
-                        <AIToolUseBatchItem key={idx} part={part} effectiveApproval={effectiveApproval} />
-                    ))}
-                </div>
-                {allNeedApproval && effectiveApproval === "needs-approval" && (
-                    <AIToolApprovalButtons count={parts.length} onApprove={handleApprove} onDeny={handleDeny} />
-                )}
+                <div className="mt-1 space-y-0.5">
+                    {parts.map((part, idx) => {
+                        const partBaseApproval = userApprovalOverride || part.data.approval;
+                        const partEffectiveApproval =
+                            !isStreaming && partBaseApproval === "needs-approval"
+                                ? "timeout"
+                                : partBaseApproval;
+                        return (
+                            <AIToolUseBatchItem
+                                key={part.data.toolcallid ?? idx}
+                                part={part}
+                                effectiveApproval={partEffectiveApproval}
+                            />
+                        );
+                    })}
+                </div>
+                {allNeedApproval && (
+                    <AIToolApprovalButtons count={parts.length} onApprove={handleApprove} onDeny={handleDeny} />
+                )}
🧹 Nitpick comments (1)
frontend/app/aipanel/aimessage.tsx (1)

249-251: Optional: show explicit message when user denies

Currently only timeouts show “Not approved.” Consider surfacing “Denied by user” for clarity.

-                {(toolData.errormessage || effectiveApproval === "timeout") && (
-                    <div className="text-sm text-red-300 mt-1">{toolData.errormessage || "Not approved"}</div>
-                )}
+                {(toolData.errormessage || effectiveApproval === "timeout" || (userApprovalOverride || toolData.approval) === "user-denied") && (
+                    <div className="text-sm text-red-300 mt-1">
+                        {toolData.errormessage ||
+                            ((userApprovalOverride || toolData.approval) === "user-denied" ? "Denied by user" : "Not approved")}
+                    </div>
+                )}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46ee88b and f141131.

📒 Files selected for processing (1)
  • frontend/app/aipanel/aimessage.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/aipanel/aimessage.tsx (3)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
🪛 Biome (2.1.2)
frontend/app/aipanel/aimessage.tsx

[error] 144-144: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
frontend/app/aipanel/aimessage.tsx (2)

13-21: Thinking message fix LGTM

Optional message prop + dots-only fallback is clean and solves the “AI is thinking” text issue.


383-413: Thinking-state derivation LGTM

Priority for pending approvals, reasoning step, and trailing text is correct and matches UX expectations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
electron.vite.config.ts (1)

133-147: Package name patterns still miss scoped packages and risk false positives.

This issue was flagged in two previous review comments. The current substring patterns:

  • Check for monaco instead of the actual package monaco-editor
  • Check for @monaco instead of the actual scope @monaco-editor
  • Check for @mermaid instead of the actual scope @mermaid-js
  • Check for @shiki instead of the actual scope @shikijs

While substring matching may catch some cases, it's imprecise and could incorrectly chunk unrelated packages (e.g., a hypothetical monaco-fork or @monaco-custom).

Apply this diff to use precise package names and scopes:

                 output: {
                     manualChunks(id) {
                         const p = id.replace(/\\/g, "/");
-                        if (p.includes("node_modules/monaco") || p.includes("node_modules/@monaco")) return "monaco";
-                        if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid"))
+                        if (p.includes("/node_modules/monaco-editor") || p.includes("/node_modules/@monaco-editor/"))
+                            return "monaco";
+                        if (p.includes("/node_modules/mermaid") || p.includes("/node_modules/@mermaid-js/"))
                             return "mermaid";
-                        if (p.includes("node_modules/katex") || p.includes("node_modules/@katex")) return "katex";
-                        if (p.includes("node_modules/shiki") || p.includes("node_modules/@shiki")) {
+                        if (p.includes("/node_modules/katex") || p.includes("/node_modules/@katex/")) return "katex";
+                        if (p.includes("/node_modules/shiki") || p.includes("/node_modules/@shikijs/")) {
                             return "shiki";
                         }
-                        if (p.includes("node_modules/cytoscape") || p.includes("node_modules/@cytoscape"))
+                        if (p.includes("/node_modules/cytoscape") || p.includes("/node_modules/@cytoscape/"))
                             return "cytoscape";
                         return undefined;
                     },
                 },
🧹 Nitpick comments (5)
frontend/app/aipanel/aimessage.tsx (5)

83-95: Explicit button type to avoid accidental form submit

Add type="button" to both buttons (defensive best practice).

-            <button
+            <button
+                type="button"
                 onClick={onApprove}
@@
-            <button
+            <button
+                type="button"
                 onClick={onDeny}

101-128: Per-item timeout/error labeling (future-proof)

Batch currently passes group-wide effectiveApproval to each item. Given batches are split by approval, this is fine. To future-proof for mixed states, derive per-item effectiveApproval in the map and pass it down.

Based on learnings

-interface AIToolUseBatchItemProps {
-    part: WaveUIMessagePart & { type: "data-tooluse" };
-    effectiveApproval: string;
-}
+interface AIToolUseBatchItemProps {
+    part: WaveUIMessagePart & { type: "data-tooluse" };
+    effectiveApproval: string; // now computed per-item
+}

And where items are rendered (see lines 182-184):

-    {parts.map((part, idx) => (
-        <AIToolUseBatchItem key={idx} part={part} effectiveApproval={effectiveApproval} />
-    ))}
+    {parts.map((part, idx) => {
+        const partBase = (userApprovalOverride || part.data.approval) as string;
+        const partEff =
+            !isStreaming && partBase === "needs-approval" ? "timeout" : partBase;
+        return (
+            <AIToolUseBatchItem key={idx} part={part} effectiveApproval={partEff} />
+        );
+    })}

134-155: Hooks order: looks good; optional defensive guard

No early return before hooks anymore. Since AIToolUseBatch assumes non-empty parts (and AIToolUseGroup enforces it), this is safe. Optionally, add a post-effect guard to avoid parts[0] access if reused elsewhere.

Based on learnings

 const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => {
     const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
+    const hasParts = parts.length > 0;
 
-    const firstTool = parts[0].data;
+    // effect first; guards on hasParts
     useEffect(() => {
-        if (!isStreaming || effectiveApproval !== "needs-approval") return;
+        if (!isStreaming || !hasParts) return;
+        // rest unchanged…
     }, [isStreaming, /* include */ hasParts, /* …existing deps */]);
 
+    if (!hasParts) return null;
+    const firstTool = parts[0].data;

354-379: Consider memoizing grouping

groupMessageParts runs each render. For long message streams, memoize on displayParts reference.

-import { memo, useEffect, useState } from "react";
+import { memo, useEffect, useMemo, useState } from "react";
@@
-const groupedParts = groupMessageParts(displayParts);
+const groupedParts = useMemo(() => groupMessageParts(displayParts), [displayParts]);

434-446: Use stable keys to reduce re-mounts during streaming

Index keys can cause unnecessary remounts when parts are appended/mutated. Prefer stable keys.

-{groupedParts.map((group, index: number) =>
-    group.type === "toolgroup" ? (
-        <AIToolUseGroup key={index} parts={group.parts} isStreaming={isStreaming} />
-    ) : (
-        <div key={index} className="mt-2">
-            <AIMessagePart part={group.part} role={message.role} isStreaming={isStreaming} />
-        </div>
-    )
-)}
+{groupedParts.map((group, index: number) => {
+    const key =
+        group.type === "toolgroup"
+            ? group.parts.map((p) => (p as any).data?.toolcallid || index).join(",")
+            : ((group.part as any).data?.toolcallid ?? `single-${index}`);
+    return group.type === "toolgroup" ? (
+        <AIToolUseGroup key={key} parts={group.parts} isStreaming={isStreaming} />
+    ) : (
+        <div key={key} className="mt-2">
+            <AIMessagePart part={group.part} role={message.role} isStreaming={isStreaming} />
+        </div>
+    );
+})}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f141131 and a732267.

📒 Files selected for processing (2)
  • electron.vite.config.ts (1 hunks)
  • frontend/app/aipanel/aimessage.tsx (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: sawka
PR: wavetermdev/waveterm#2430
File: frontend/app/aipanel/aimessage.tsx:137-144
Timestamp: 2025-10-14T06:30:54.751Z
Learning: In `frontend/app/aipanel/aimessage.tsx`, the `AIToolUseGroup` component splits file operation tool calls into separate batches (`fileOpsNeedApproval` and `fileOpsNoApproval`) based on their approval state before passing them to `AIToolUseBatch`. This ensures each batch has homogeneous approval states, making group-level approval handling valid.
📚 Learning: 2025-10-14T06:30:54.751Z
Learnt from: sawka
PR: wavetermdev/waveterm#2430
File: frontend/app/aipanel/aimessage.tsx:137-144
Timestamp: 2025-10-14T06:30:54.751Z
Learning: In `frontend/app/aipanel/aimessage.tsx`, the `AIToolUseGroup` component splits file operation tool calls into separate batches (`fileOpsNeedApproval` and `fileOpsNoApproval`) based on their approval state before passing them to `AIToolUseBatch`. This ensures each batch has homogeneous approval states, making group-level approval handling valid.

Applied to files:

  • frontend/app/aipanel/aimessage.tsx
🧬 Code graph analysis (1)
frontend/app/aipanel/aimessage.tsx (4)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (24-24)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (537-537)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
pkg/aiusechat/uctypes/usechat-types.go (2)
  • AIMessage (263-266)
  • AIMessagePart (268-284)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (5)
frontend/app/aipanel/aimessage.tsx (5)

13-21: AIThinking: correct conditional text rendering

Good fix: empty string yields dots-only; null hides component.


186-189: Approve/Deny visibility condition is correct

Buttons shown only when allNeedApproval and still awaiting approval; matches UX intent for batch.


247-252: Single-tool “Not approved” handling

Timeout-to-“Not approved” logic is consistent with batch behavior. Keepalive effect is correctly scoped by effectiveApproval.


265-306: Good split by approval state before batching

Separating file ops into need-approval vs no-approval ensures batch semantics are homogeneous; this validates group-level keepalive and CTA visibility.

Based on learnings


381-410: Thinking message logic reads well

Priority for pending approvals, then reasoning, then dots-only; hides when streaming text arrives. Nice.

@sawka sawka merged commit 2480ebe into main Oct 14, 2025
8 checks passed
@sawka sawka deleted the sawka/batch-approval branch October 14, 2025 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant